// Copyright (c) 2012, yuno. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
//    this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
//    this list of conditions and the following disclaimer in the documentation
//    and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of contributors
//    may be used to endorse or promote products derived from this software
//    without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
package sugarmintcandy.widgetwindow;

import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;

import com.android.vending.billing.IMarketBillingService;

public class BillingService implements ServiceConnection, Handler.Callback {
    // Request bundle key
    public static final String KEY_BILLING_REQUEST = "BILLING_REQUEST";
    public static final String KEY_API_VERSION = "API_VERSION";
    public static final String KEY_PACKAGE_NAME = "PACKAGE_NAME";
    public static final String KEY_ITEM_ID = "ITEM_ID";
    public static final String KEY_NONCE = "NONCE";
    public static final String KEY_NOTIFY_IDS = "NOTIFY_IDS";
    public static final String KEY_DEVELOPER_PAYLOAD = "DEVELOPER_PAYLOAD";

    // Request code
    public static final String REQUEST_CHECK_BILLING_SUPPORTED = "CHECK_BILLING_SUPPORTED";
    public static final String REQUEST_REQUEST_PURCHASE = "REQUEST_PURCHASE";
    public static final String REQUEST_GET_PURCHASE_INFORMATION = "GET_PURCHASE_INFORMATION";
    public static final String REQUEST_CONFIRM_NOTIFICATIONS = "CONFIRM_NOTIFICATIONS";
    public static final String REQUEST_RESTORE_TRANSACTIONS = "RESTORE_TRANSACTIONS";

    // Response key
    public static final String KEY_RESPONSE_CODE = "RESPONSE_CODE";
    public static final String KEY_PURCHASE_INTENT = "PURCHASE_INTENT";
    public static final String KEY_REQUEST_ID = "REQUEST_ID";

    // Response code
    public static final int RESPONSE_OK = 0;
    public static final int RESPONSE_USER_CANCELED = 1;
    public static final int RESPONSE_SERVICE_UNAVAILABLE = 2;
    public static final int RESPONSE_BILLING_UNAVAILABLE = 3;
    public static final int RESPONSE_ITEM_UNAVAILABLE = 4;
    public static final int RESPONSE_DEVELOPER_ERROR = 5;
    public static final int RESPONSE_ERROR = 6;

    // Service action
    public static final String ACTION_BIND = "com.android.vending.billing.MarketBillingService.BIND";

    // Broadcast action
    public static final String ACTION_RESPONSE_CODE = "com.android.vending.billing.RESPONSE_CODE";
    public static final String ACTION_IN_APP_NOTIFY = "com.android.vending.billing.IN_APP_NOTIFY";
    public static final String ACTION_PURCHASE_STATE_CHANGED = "com.android.vending.billing.PURCHASE_STATE_CHANGED";

    // Broadcast extra key
    public static final String EXTRA_REQUEST_ID = "request_id";
    public static final String EXTRA_RESPONSE_CODE = "response_code";
    public static final String EXTRA_NOTIFICATION_ID = "notification_id";
    public static final String EXTRA_INAPP_SIGNED_DATA = "inapp_signed_data";
    public static final String EXTRA_INAPP_SIGNATURE = "inapp_signature";

    // JSON fields
    public static final String JSON_NONCE = "nonce";
    public static final String JSON_ORDERS = "orders";
    public static final String JSON_NOTIFICATION_ID = "notificationId";
    public static final String JSON_ORDER_ID = "orderId";
    public static final String JSON_PACKAGE_NAME = "packageName";
    public static final String JSON_PRODUCT_ID = "productId";
    public static final String JSON_PURCHASE_TIME = "purchaseTime";
    public static final String JSON_PURCHASE_STATE = "purchaseState";
    public static final String JSON_PURCHASE_TOKEN = "purchaseToken";
    public static final String JSON_DEVELOPER_PAYLOAD = "developerPayload";

    // Purchase state
    public static final int STATE_UNPARCHASED = -1;
    public static final int STATE_PURCHASED = 0;
    public static final int STATE_CANCELED = 1;
    public static final int STATE_REFUNDED = 2;
    public static final int STATE_EXPIRED = 3;

    // Miscellaneous
    public static final int VALUE_API_VERSION = 1;
    public static final int IDLE_TIMEOUT = 10000;

    private Context mContext;
    private IMarketBillingService mService;
    private ArrayList<PurchaseStateListener> mListeners;
    private HashMap<Long, BillingResponseHandler> mRequestIdMap;
    private HashSet<String> mInAppNotifyIds;
    private HashSet<Long> mNonces;
    private LinkedList<Runnable> mTasks;
    private Handler mHandler;
    private boolean mBound;
    private PublicKey mPublicKey;

    public BillingService(Context context) {
	Debug.trace();
	mContext = context;
	mListeners = new ArrayList<PurchaseStateListener>();
	mRequestIdMap = new HashMap<Long, BillingResponseHandler>();
	mInAppNotifyIds = new HashSet<String>();
	mNonces = new HashSet<Long>();
	mTasks = new LinkedList<Runnable>();
	mHandler = new Handler(this);
    }

    public void shutdown() {
	Debug.trace();
	unbind();
	mHandler.removeMessages(R.id.start);
	mHandler.removeMessages(R.id.stop);
	mListeners.clear();
	mRequestIdMap.clear();
	mInAppNotifyIds.clear();
	mNonces.clear();
	mTasks.clear();
    }

    private boolean bind() {
	if (!mBound) {
	    Debug.trace();
	    mBound = mContext.bindService(new Intent(ACTION_BIND), this,
		    Context.BIND_AUTO_CREATE);
	    Debug.trace("bindService()=%s", mBound);
	}
	return mBound;
    }

    private void unbind() {
	if (mBound) {
	    Debug.trace();
	    mContext.unbindService(this);
	    mBound = false;
	    mService = null;
	}
    }

    private void post(Runnable task) {
	Debug.trace("task=%s", task);
	mTasks.add(task);
	mHandler.removeMessages(R.id.stop);
	mHandler.sendEmptyMessage(R.id.start);
    }

    @Override
    public boolean handleMessage(Message msg) {
	int what = msg.what;
	if (what == R.id.start) {
	    if (mService == null) {
		if (bind()) {
		    return true;
		}
	    }
	    Runnable task = mTasks.poll();
	    if (task != null) {
		task.run();
		mHandler.sendEmptyMessage(R.id.start);
	    } else {
		Debug.trace("No more tasks");
		mHandler.sendEmptyMessageDelayed(R.id.stop, IDLE_TIMEOUT);
	    }
	}
	if (what == R.id.stop) {
	    unbind();
	}
	return true;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder binder) {
	Debug.trace();
	mService = IMarketBillingService.Stub.asInterface(binder);
	mHandler.sendEmptyMessage(R.id.start);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
	Debug.trace();
	mService = null;
    }

    public void checkBillingSupported(final BillingResponseHandler handler) {
	Debug.trace("handler=%s", handler);
	post(new Runnable() {
	    @Override
	    public void run() {
		if (mService == null) {
		    fireBillingResponse(handler, RESPONSE_BILLING_UNAVAILABLE);
		} else {
		    try {
			Bundle b = new Bundle();
			b.putInt(KEY_API_VERSION, VALUE_API_VERSION);
			b.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
			b.putString(KEY_BILLING_REQUEST,
				REQUEST_CHECK_BILLING_SUPPORTED);
			b = mService.sendBillingRequest(b);
			int responseCode = b.getInt(KEY_RESPONSE_CODE,
				RESPONSE_ERROR);
			fireBillingResponse(handler, responseCode);
		    } catch (RemoteException ex) {
			Debug.warn(ex);
			fireBillingResponse(handler,
				RESPONSE_BILLING_UNAVAILABLE);
		    }
		}
	    }
	});
    }

    public void requestPurchase(final Activity activity, final String itemId,
	    final BillingResponseHandler handler) {
	Debug.trace("itemId=%s, handler=%s", itemId, handler);
	post(new Runnable() {
	    @Override
	    public void run() {
		if (mService == null) {
		    fireBillingResponse(handler, RESPONSE_BILLING_UNAVAILABLE);
		} else {
		    try {
			Bundle b = new Bundle();
			b.putInt(KEY_API_VERSION, VALUE_API_VERSION);
			b.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
			b.putString(KEY_BILLING_REQUEST,
				REQUEST_REQUEST_PURCHASE);
			b.putString(KEY_ITEM_ID, itemId);
			b = mService.sendBillingRequest(b);
			int responseCode = b.getInt(KEY_RESPONSE_CODE,
				RESPONSE_ERROR);
			if (responseCode == RESPONSE_OK) {
			    PendingIntent intent = b
				    .getParcelable(KEY_PURCHASE_INTENT);
			    if (ApiHelper.SDK_INT >= 5) {
				ApiHelper.startIntentSender(activity, intent,
					null, 0, 0, 0);
			    } else {
				intent.send(mContext, 0, null);
			    }
			    long requestId = b.getLong(KEY_REQUEST_ID);
			    mRequestIdMap.put(requestId, handler);
			} else {
			    fireBillingResponse(handler, responseCode);
			}
		    } catch (RemoteException ex) {
			Debug.warn(ex);
			fireBillingResponse(handler,
				RESPONSE_BILLING_UNAVAILABLE);
		    } catch (PendingIntent.CanceledException ex) {
			Debug.warn(ex);
			fireBillingResponse(handler, RESPONSE_ERROR);
		    }
		}
	    }
	});
    }

    public void getPurchaseInformation(final String[] notifyIds,
	    final BillingResponseHandler handler) {
	Debug.trace("notifyIds=%s, handler=%s", notifyIds, handler);
	post(new Runnable() {
	    @Override
	    public void run() {
		if (mService == null) {
		    fireBillingResponse(handler, RESPONSE_BILLING_UNAVAILABLE);
		} else {
		    try {
			final long nonce = Utilities.randomLong();
			Bundle b = new Bundle();
			b.putInt(KEY_API_VERSION, VALUE_API_VERSION);
			b.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
			b.putString(KEY_BILLING_REQUEST,
				REQUEST_GET_PURCHASE_INFORMATION);
			b.putStringArray(KEY_NOTIFY_IDS, notifyIds);
			b.putLong(KEY_NONCE, nonce);
			b = mService.sendBillingRequest(b);
			int result = b
				.getInt(KEY_RESPONSE_CODE, RESPONSE_ERROR);
			if (result == RESPONSE_OK) {
			    long requestId = b.getLong(KEY_REQUEST_ID);
			    mNonces.add(nonce);
			    Debug.trace("requestId=%d, nonce=%d", requestId,
				    nonce);
			    mRequestIdMap.put(requestId,
				    new BillingResponseHandler() {
					@Override
					public void onBillingResponse(
						int responseCode) {
					    if (responseCode != RESPONSE_OK) {
						mNonces.remove(nonce);
					    }
					    fireBillingResponse(handler,
						    responseCode);
					}
				    });
			} else {
			    fireBillingResponse(handler, result);
			}
		    } catch (RemoteException ex) {
			Debug.warn(ex);
			fireBillingResponse(handler,
				RESPONSE_BILLING_UNAVAILABLE);
		    }
		}
	    }
	});
    }

    public void confirmNotifications(final String[] notifyIds,
	    final BillingResponseHandler handler) {
	Debug.trace("notifyIds=%s, handler=%s", notifyIds, handler);
	post(new Runnable() {
	    @Override
	    public void run() {
		if (mService == null) {
		    fireBillingResponse(handler, RESPONSE_BILLING_UNAVAILABLE);
		} else {
		    try {
			Bundle b = new Bundle();
			b.putInt(KEY_API_VERSION, VALUE_API_VERSION);
			b.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
			b.putString(KEY_BILLING_REQUEST,
				REQUEST_CONFIRM_NOTIFICATIONS);
			b.putStringArray(KEY_NOTIFY_IDS, notifyIds);
			b = mService.sendBillingRequest(b);
			int responseCode = b.getInt(KEY_RESPONSE_CODE,
				RESPONSE_ERROR);
			if (responseCode == RESPONSE_OK) {
			    long requestId = b.getLong(KEY_REQUEST_ID);
			    mRequestIdMap.put(requestId, handler);
			} else {
			    fireBillingResponse(handler, responseCode);
			}
		    } catch (RemoteException ex) {
			Debug.warn(ex);
			fireBillingResponse(handler,
				RESPONSE_BILLING_UNAVAILABLE);
		    }
		}
	    }
	});
    }

    public void restoreTransactions(final BillingResponseHandler handler) {
	Debug.trace("handler=%s", handler);
	post(new Runnable() {
	    @Override
	    public void run() {
		if (mService == null) {
		    fireBillingResponse(handler, RESPONSE_BILLING_UNAVAILABLE);
		} else {
		    try {
			final long nonce = Utilities.randomLong();
			Bundle b = new Bundle();
			b.putInt(KEY_API_VERSION, VALUE_API_VERSION);
			b.putString(KEY_PACKAGE_NAME, mContext.getPackageName());
			b.putString(KEY_BILLING_REQUEST,
				REQUEST_RESTORE_TRANSACTIONS);
			b.putLong(KEY_NONCE, nonce);
			b = mService.sendBillingRequest(b);
			int responseCode = b.getInt(KEY_RESPONSE_CODE,
				RESPONSE_ERROR);
			if (responseCode == RESPONSE_OK) {
			    long requestId = b.getLong(KEY_REQUEST_ID);
			    Debug.trace("requestId=%d, nonce=%d", requestId,
				    nonce);
			    mNonces.add(nonce);
			    mRequestIdMap.put(requestId,
				    new BillingResponseHandler() {
					@Override
					public void onBillingResponse(
						int responseCode) {
					    if (responseCode != RESPONSE_OK) {
						mNonces.remove(nonce);
					    }
					    fireBillingResponse(handler,
						    responseCode);
					}
				    });
			} else {
			    fireBillingResponse(handler, responseCode);
			}
		    } catch (RemoteException ex) {
			Debug.warn(ex);
			fireBillingResponse(handler,
				RESPONSE_BILLING_UNAVAILABLE);
		    }
		}
	    }
	});
    }

    public void registerPurchaseStateListener(PurchaseStateListener l) {
	mListeners.add(l);
    }

    public void unregisterPurchaseStateListener(PurchaseStateListener l) {
	mListeners.remove(l);
    }

    public void onResponseCode(Intent intent) {
	int responseCode = intent.getIntExtra(EXTRA_RESPONSE_CODE,
		RESPONSE_ERROR);
	long requestId = intent.getLongExtra(EXTRA_REQUEST_ID, -1);
	BillingResponseHandler handler = mRequestIdMap.remove(requestId);
	Debug.trace("requestId=%s, responseCode=%d, handler=%s", requestId,
		responseCode, handler);
	fireBillingResponse(handler, responseCode);
    }

    public void onInAppNotify(Context context, Intent intent) {
	final String notificationId = intent
		.getStringExtra(EXTRA_NOTIFICATION_ID);
	Debug.trace("notificationId=%s", notificationId);
	if (notificationId != null) {
	    mInAppNotifyIds.add(notificationId);
	    String[] notifyIds = { notificationId };
	    getPurchaseInformation(notifyIds, null);
	}
    }

    public void onPurchaseStateChanged(Context context, Intent intent) {
	String data = intent.getStringExtra(EXTRA_INAPP_SIGNED_DATA);
	String signature = intent.getStringExtra(EXTRA_INAPP_SIGNATURE);
	Debug.trace("data=%s, signature=%s", data, signature);
	if (data == null || signature == null) {
	    return;
	}
	try {
	    Signature signer = Signature.getInstance("SHA1withRSA");
	    signer.initVerify(getPublicKey());
	    signer.update(data.getBytes());
	    if (!signer.verify(Utilities.decodeBase64(signature))) {
		Debug.trace("Verification failed");
		return;
	    }
	} catch (GeneralSecurityException ex) {
	    Debug.warn(ex);
	}
	ArrayList<String> notifyIds = new ArrayList<String>();
	try {
	    JSONObject json = new JSONObject(data);
	    long nonce = json.getLong(JSON_NONCE);
	    if (!mNonces.remove(nonce)) {
		Debug.trace("Unknown nonce: %d", nonce);
		return;
	    }
	    JSONArray orders = json.getJSONArray(JSON_ORDERS);
	    for (int i = 0, n = orders.length(); i < n; i++) {
		try {
		    JSONObject order = orders.getJSONObject(i);
		    String notificationId = order.optString(
			    JSON_NOTIFICATION_ID, null);
		    String orderId = order.getString(JSON_ORDER_ID);
		    String productId = order.getString(JSON_PRODUCT_ID);
		    int state = order.getInt(JSON_PURCHASE_STATE);
		    firePurchaseStateChanged(orderId, productId, state);
		    if (notificationId != null
			    && mInAppNotifyIds.remove(notificationId)) {
			notifyIds.add(notificationId);
		    }
		} catch (JSONException ex) {
		    Debug.warn(ex);
		}
	    }
	} catch (JSONException ex) {
	    Debug.warn(ex);
	}
	if (!notifyIds.isEmpty()) {
	    confirmNotifications(
		    notifyIds.toArray(new String[notifyIds.size()]), null);
	}
    }

    private PublicKey getPublicKey() throws GeneralSecurityException {
	if (mPublicKey == null) {
	    byte[] key = Utilities.decodeBase64(Constants.MARKET_PUBLIC_KEY);
	    X509EncodedKeySpec spec = new X509EncodedKeySpec(key);
	    KeyFactory factory = KeyFactory.getInstance("RSA");
	    mPublicKey = factory.generatePublic(spec);
	}
	return mPublicKey;
    }

    private void fireBillingResponse(BillingResponseHandler handler,
	    int responseCode) {
	Debug.trace("handler=%s, responseCode=%d", handler, responseCode);
	if (handler != null) {
	    handler.onBillingResponse(responseCode);
	}
    }

    public void firePurchaseStateChanged(String orderId, String productId,
	    int state) {
	Debug.trace("orderId=%s, productId=%s, state=%d", orderId, productId,
		state);
	for (PurchaseStateListener l : mListeners) {
	    l.onPurchaseStateChanged(orderId, productId, state);
	}
    }
}
